import socket
import select
import threading
import cPickle as pickle
import os,errno
from stat import *

# default addr of directory/lockserver
DFS_DIRPORT = 20000
DFS_DIRHOST = 'localhost'
DFS_DIRADDR = (DFS_DIRHOST, DFS_DIRPORT)
DFS_DIRCONFIG = 'config.txt'

# used by proxy cache
DFS_BUFSIZE = 8192
DFS_PAGESIZE = 4096
DFS_MAXPAGE = 1024

# check interval 10 secs
DFS_INTERVAL = 10

DFS_REQUEST = 0
DFS_REPLY = 1
DFS_EVENT = 2

FTYPE_UNKNOWN = 0
FTYPE_DIR= 1
FTYPE_FILE = 2
        
ftype2str = { 
    FTYPE_UNKNOWN : 'U',
    FTYPE_DIR : 'D',
    FTYPE_FILE : 'F'
}

# reserved fids
DFS_ROOTDIR = 'root'

LOCK_NONE = 0
LOCK_WATCH = 1
LOCK_READ = 2
LOCK_WRITE = 3

ltype2str = { 
    LOCK_NONE  : 'none',
    LOCK_WATCH : 'watch',
    LOCK_READ  : 'read',
    LOCK_WRITE : 'write'
}

def fid2str(fid):
    return '%08X' % fid
    
def get_at(alist, idx, default):
    if idx < len(alist):
        value = alist[idx]
    else:
        value = default
    return value


# convert a str to number else return None
def str2num(str, base=10):
    try:
        num = int(str, base)
    except (ValueError, TypeError):
        num = None
    return num

def addr2str(addr):
    return '%s:%d' %( addr[0], addr[1])

def get_dirname(path):
    name = os.path.dirname(path)
    if len(name) == 0:
        name = '/'
    return name

def get_basename(path):
    return os.path.basename(path)

# spit a path into if directory components ignoring extras slashes
def split_path(path):
    parts = []

    # eat slashes
    path = path.lstrip('/')

    while len(path):
        # extract next part
        idx = path.find('/')
        if idx == -1:
            dname = path
            path = ''
        else:
            dname = path[:idx]
            path = path[idx:]

        parts.append(dname)

        # eat slashes
        path = path.lstrip('/')

    return parts

# remove extra slashes from path name
def sanitize(path):
    parts = split_path(path)
    newpath = '/' + '/'.join(parts)
    return newpath

# helper function to debug call on the server side
def args2str(args):
    slist = []
    for arg in args:
        if type(arg) is str:
            if len(arg) > 50:
                # data? display only the first 20
                astr = arg[:20]
            else:
                astr = arg
        elif type(arg) is list:
            astr = repr(arg[:2])
        else:
            astr = repr(arg)
        slist.append(astr)
    return slist

# base class for all system errors
class DFSError(Exception):
    pass

# used to report socket connection errors
class ConnError(DFSError):
    def __init__(self, rc, reason):
        self.rc = rc
        self.reason = reason
    def __str__(self):
        return '%d %s' %( self.rc, self.reason)

# used to report a failed request
class RequestError(ConnError):
    pass

# used to indicate a broken socket connection
class ConnBroke(DFSError):
     pass

class BadName(DFSError):
     pass

# doubly linked list node
class Node:
 
    def __init__(self, data):
        self.prev = None
        self.next = None

    def insertBetweenLRU(self, leftLink, rightLink):
        self.prev= leftLink
        self.next= rightLink
        left.next= self
        right.prev= self

    def insertBeforeLRU(self, node):
        self.insertBetween(node.prev, node)

    def insertAfter(self, node):
        self.insertBetween(node,node.next)
        
    def remove(self):
        # point prev node to next
        if (self.prev):
            self.prev.next= self.next
        # point next to prev
        if (self.next):
            self.next.prev= self.prev
        self.prev= None
        self.next= None

# used to hold a file attrbutes
class FileAttr:

    def __init__(self):
        self.ftype = FTYPE_UNKNOWN
        self.mtime = 0
        self.size = 0
        self.fid = ''
        self.server = ''
        self.raw_path = ''

    def __str__(self):
        return 'mtime=%s size=%s fid=%s' %( 
            str(self.mtime), str(self.size), self.fid
        )

    def isDir(self):
        return self.ftype == FTYPE_DIR

    def isFile(self):
        return self.ftype == FTYPE_FILE

    # check if file is unchanged by comparing relevant attributes
    def match_mattr(self, fattr):
        if not fattr: return False
        if self.mtime != fattr.mtime: return False
        if self.size != fattr.size: return False
        return True


#
# Adapts a TCP socket into a rpc message channel where
# requests, replys and events can be sent and received
# A seperate thread is run to receive replys and events.
# Client sent request over a 
#

class MessageSocket(threading.Thread):

    def __init__(self, sock, addr, debug=False):

        threading.Thread.__init__(self)
        self.daemon = True

        # we set this to speed up rpc comms
        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

        self.sock = sock
        self.addr = addr
        self.debug = debug
        self._closed = False

        self.recv_buf = '' 

        # used by thread for replys
        self.replys = {}
        self.cvars = {}
        self.seqno = 0
       
        # used to forward events 
        self.event_handler = None

    def __str__(self):
        return addr2str(self.addr)

    def fileno(self):
        return self.sock.fileno()

    # close the underlying socket
    def close(self):
        # joker check
        if self._closed: return
        # first close socket
        self.sock.close()
        self._closed = True
        # unblock client waiting for reply
        seqno_list = self.cvars.keys()
        for seqno in seqno_list:
            cvar = self.cvars[seqno]
            cvar.acquire()
            self.replys[seqno] = (1, 'closed')
            cvar.notify()
            cvar.release()

        # inform subclass
        self.handle_closed()

    def closed(self):
        return self._closed

    def get_addr(self):
        return self.addr

    # register a handler for rpc events sent by the server
    def set_event_handler(self, handler):
        self.event_handler = handler

    # send a packet over the socket
    def send_data(self, data):
        data_len = len(data)
        total_sent = 0
        while total_sent < data_len:
            num_sent = self.sock.send(data[total_sent:])
            if num_sent == 0:
                raise ConnBroke
            total_sent += num_sent

    # recive a packet from the socket
    def recv_data(self, data_len):
        buffers = []
        data_recv = 0
        while data_recv < data_len:
            data = self.sock.recv(data_len-data_recv)
            if data == '':
                raise ConnBroke
            buffers.append(data)
            data_recv += len(data)
        return ''.join(buffers)

    #
    # encode a msg into a pdu and send it over the socket
    #
    def send_pdu(self, pdu_type, seqno, msg):
        if self.debug: print 'send-pdu',msg
        data = pickle.dumps(msg, 1)
        # first send header
        hdr_str = '%08X%08X%08X' % (len(data), seqno, pdu_type)
        if self.debug: print 'send-hdr',hdr_str
        self.send_data(hdr_str)
        # now send payload
        if self.debug: print 'send-data',data
        self.send_data(data)

    #
    # receive a pdu from the socket and decode it to a msg
    #
    def recv_pdu(self):
        # first recv hdr
        hdr_str = self.recv_data(24)
        if self.debug: print 'recv-hdr',hdr_str
        # decode hdr
        data_len = int(hdr_str[0:8], 16)
        seqno = int(hdr_str[8:16], 16)
        pdu_type = int(hdr_str[16:24], 16)
        # now recv payload
        data = self.recv_data(data_len)
        if self.debug: print 'recv-data', data
        msg = pickle.loads(data)
        if self.debug: print 'recv-msg', msg
        return pdu_type, seqno, msg

    # generate the next sequence number for a request
    def next_seqno(self):
        seqno = self.seqno
        self.seqno += 1
        return seqno

    def handle_closed(self):
        pass

    def handle_error(self):
        print '-'*40
        print 'Exception processing request from', self.addr
        import traceback
        traceback.print_exc() # XXX But this goes to stderr!
        print '-'*40

    # call event handler if registered
    def handle_event(self, event):
        name, args = event
        mname = 'handle_' + name
        if hasattr(self.event_handler, mname):
            method = getattr(self.event_handler, mname)
            method(*args)

    def handle_reply(self, seqno, msg):
        # tell blocked client we have a reply
        cvar = self.cvars.get(seqno, None)
        if cvar:
            cvar.acquire()
            self.replys[seqno] = msg
            cvar.notify()
            cvar.release()

    #
    # calls the reqeusted method in the server subclass
    #
    def handle_request(self, seqno, req):
        name,args = req
        print 'Client', self.addr, 'call', name, args2str(args)
        if hasattr(self, name):
            method = getattr(self, name)
            result = method(*args)
        else: 
            result = (errno.EOPNOTSUPP,'')

        self.send_pdu(DFS_REPLY, seqno, result)

    # read events and responses
    def run(self):
        try:
            while self.sock:
                (pdu_type,seqno,msg) = self.recv_pdu()
                if pdu_type == DFS_REQUEST:
                    reply = self.handle_request(seqno, msg)
                elif pdu_type == DFS_REPLY:
                    self.handle_reply(seqno, msg)
                elif pdu_type == DFS_EVENT:
                    self.handle_event(msg)
                else:
                    print '!!!Unknown pdu type!!!', pdu_type, seqno, msg
        except ConnBroke:
            pass
        except:
            self.handle_error()

        # tell other endpoint to stop
        self.close()

    def send_event(self, event):
        self.send_pdu(DFS_EVENT, 0, event)

    def send_request(self, request):
        seqno = self.next_seqno()
        cvar = threading.Condition()
        self.cvars[seqno] = cvar
        self.send_pdu(DFS_REQUEST, seqno, request)
        return seqno

    # wait for reader thread to tell us when reply received
    def wait_reply(self, seqno):
        cvar = self.cvars[seqno]
        cvar.acquire()
        while seqno not in self.replys:
            cvar.wait()
        reply = self.replys[seqno]
        del self.replys[seqno]
        del self.cvars[seqno]
        cvar.release()
        return reply

    #
    # used by client to call a method remotely
    #
    def call(self, name, *args):
       
        try:
            seqno = self.send_request((name, args))
            reply = self.wait_reply(seqno)
        except socket.error, e:
            self.close()
            raise ConnError(e.errno, addr2str(self.addr))
        except ConnBroke:
            self.close()
            raise ConnError(errno.ECONNABORTED, addr2str(self.addr))
        
        rc,result = reply
        if rc !=0:
            raise RequestError(rc, result)

        return result

